اكتشف أنماط حقن التبعية المتقدمة في FastAPI لبناء تطبيقات قابلة للتطوير والصيانة والاختبار. تعلم كيفية هيكلة حاوية DI قوية.
حقن التبعية في FastAPI: بنية حاوية DI متقدمة
أصبح FastAPI، بتصميمه البديهي وميزاته القوية، المفضل لبناء واجهات برمجة تطبيقات الويب الحديثة في بايثون. تكمن إحدى نقاط قوته الأساسية في تكامله السلس مع حقن التبعية (DI)، مما يمكّن المطورين من إنشاء تطبيقات مفكوكة الاقتران وقابلة للاختبار والصيانة. في حين أن نظام DI المدمج في FastAPI ممتاز لحالات الاستخدام البسيطة، غالبًا ما تستفيد المشاريع الأكثر تعقيدًا من بنية حاوية DI أكثر هيكلة وتقدمًا. تستكشف هذه المقالة استراتيجيات مختلفة لبناء مثل هذه البنية، وتقدم أمثلة عملية ورؤى لتصميم تطبيقات قوية وقابلة للتطوير.
فهم حقن التبعية (DI) وعكس التحكم (IoC)
قبل الغوص في بنيات حاوية DI المتقدمة، دعنا نوضح المفاهيم الأساسية:
- حقن التبعية (DI): نمط تصميم حيث يتم توفير التبعيات لمكون من مصادر خارجية بدلاً من إنشائها داخليًا. هذا يعزز الاقتران الضعيف، مما يجعل المكونات أسهل في الاختبار وإعادة الاستخدام.
- عكس التحكم (IoC): مبدأ أوسع حيث يتم عكس التحكم في إنشاء الكائنات وإدارتها - يتم تفويضها إلى إطار عمل أو حاوية. DI هو نوع محدد من IoC.
يدعم FastAPI بطبيعته DI من خلال نظام التبعية الخاص به. يمكنك تحديد التبعيات ككائنات قابلة للاستدعاء (وظائف، فئات، إلخ)، و FastAPI يحلها ويحقنها تلقائيًا في وظائف نقطة النهاية أو التبعيات الأخرى.
مثال (FastAPI DI الأساسي):
from fastapi import FastAPI, Depends
app = FastAPI()
# Dependency
def get_db():
db = {"items": []} # Simulate a database connection
try:
yield db
finally:
# Close the database connection (if needed)
pass
# Endpoint with dependency injection
@app.get("/items/")
async def read_items(db: dict = Depends(get_db)):
return db["items"]
في هذا المثال، get_db هي تبعية توفر اتصالاً بقاعدة البيانات. يستدعي FastAPI تلقائيًا get_db ويحقن النتيجة (قاموس db) في دالة نقطة النهاية read_items.
لماذا حاوية DI متقدمة؟
يعمل DI المدمج في FastAPI بشكل جيد للمشاريع البسيطة، ولكن مع تزايد تعقيد التطبيقات، توفر حاوية DI الأكثر تطورًا العديد من المزايا:
- إدارة التبعية المركزية: توفر الحاوية المخصصة مصدرًا واحدًا للحقيقة لجميع التبعيات، مما يسهل إدارة وفهم تبعيات التطبيق.
- تكوين وإدارة دورة الحياة: يمكن للحاوية التعامل مع تكوين ودورة حياة التبعيات، مثل إنشاء مفردات، وإدارة الاتصالات، والتخلص من الموارد.
- قابلية الاختبار: تعمل الحاوية المتقدمة على تبسيط الاختبار من خلال السماح لك بتجاوز التبعيات بسهولة باستخدام كائنات وهمية أو مضاعفات اختبار.
- إلغاء الاقتران: يعزز مزيدًا من الفصل بين المكونات، مما يقلل التبعيات ويحسن قابلية صيانة التعليمات البرمجية.
- الامتداد: تسمح لك الحاوية القابلة للتوسيع بإضافة ميزات وتكاملات مخصصة حسب الحاجة.
استراتيجيات بناء حاوية DI متقدمة
هناك العديد من الأساليب لبناء حاوية DI متقدمة في FastAPI. فيما يلي بعض الاستراتيجيات الشائعة:
1. استخدام مكتبة DI مخصصة (مثل `injector`, `dependency_injector`)
تتوفر العديد من مكتبات DI القوية لـ Python، مثل injector و dependency_injector. توفر هذه المكتبات مجموعة شاملة من الميزات لإدارة التبعيات، بما في ذلك:
- الربط: تحديد كيفية حل التبعيات وحقنها.
- النطاقات: التحكم في دورة حياة التبعيات (مثل المفردة والعابرة).
- التكوين: إدارة إعدادات التكوين للتبعية.
- AOP (البرمجة الموجهة للجوانب): اعتراض استدعاءات الأسلوب للمخاوف الشاملة.
مثال مع `dependency_injector`
dependency_injector هو خيار شائع لبناء حاويات DI. دعنا نوضح استخدامه بمثال:
from dependency_injector import containers, providers
from fastapi import FastAPI, Depends
# Define dependencies
class Database:
def __init__(self, connection_string: str):
self.connection_string = connection_string
# Initialize database connection
print(f"Connecting to database: {self.connection_string}")
def get_items(self):
# Simulate fetching items from the database
return [{"id": 1, "name": "Item 1"}, {"id": 2, "name": "Item 2"}]
class UserRepository:
def __init__(self, database: Database):
self.database = database
def get_all_users(self):
# Simulating database request to get all users
return [{"id": "user1", "name": "Alice"},{"id": "user2", "name": "Bob"}]
class Settings:
def __init__(self, database_url):
self.database_url = database_url
# Define container
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
settings = providers.Singleton(Settings, database_url = config.database_url)
database = providers.Singleton(Database, connection_string=config.database_url)
user_repository = providers.Factory(UserRepository, database=database)
# Create FastAPI app
app = FastAPI()
# Configure container (from an environment variable)
container = Container()
container.config.database_url.from_env("DATABASE_URL", default="sqlite:///:memory:")
container.wire([__name__]) # enables injection of dependencies into FastAPI endpoints
# Dependency for FastAPI
def get_user_repository(user_repository: UserRepository = Depends(container.user_repository.provided)) -> UserRepository:
return user_repository
# Endpoint using injected dependency
@app.get("/users/")
async def read_users(user_repository: UserRepository = Depends(get_user_repository)):
return user_repository.get_all_users()
@app.on_event("startup")
async def startup_event():
# Container initialization
container.init_resources()
شرح:
- نحن نحدد تبعياتنا (
DatabaseوUserRepositoryوSettings) كفئات Python عادية. - نحن نقوم بإنشاء فئة
Containerترث منcontainers.DeclarativeContainer. تحدد هذه الفئة التبعيات وموفريها (مثلproviders.Singletonللمفردات،providers.Factoryلإنشاء مثيلات جديدة في كل مرة). - يمكّن السطر
container.wire([__name__])حقن التبعية في نقاط نهاية FastAPI. - تستخدم الدالة
get_user_repositoryدالة FastAPI التابعةcontainer.user_repository.providedلاسترداد مثيل UserRepository من الحاوية. - تحقن دالة نقطة النهاية
read_usersالتبعيةUserRepository. - تتيح لك
configإخراج تكوينات التبعية. يمكن أن يأتي بعد ذلك من متغيرات البيئة أو ملفات التكوين وما إلى ذلك. - يستخدم
startup_eventلتهيئة الموارد المُدارة في الحاوية
2. تنفيذ حاوية DI مخصصة
لمزيد من التحكم في عملية DI، يمكنك تنفيذ حاوية DI مخصصة. يتطلب هذا النهج مزيدًا من الجهد ولكنه يسمح لك بتكييف الحاوية لتلبية احتياجاتك المحددة.
مثال على حاوية DI مخصصة أساسية:
from typing import Callable, Dict, Type, Any
from fastapi import FastAPI, Depends
class Container:
def __init__(self):
self.dependencies: Dict[Type[Any], Callable[..., Any]] = {}
self.instances: Dict[Type[Any], Any] = {}
def register(self, dependency_type: Type[Any], provider: Callable[..., Any]):
self.dependencies[dependency_type] = provider
def resolve(self, dependency_type: Type[Any]) -> Any:
if dependency_type in self.instances:
return self.instances[dependency_type]
if dependency_type not in self.dependencies:
raise Exception(f"Dependency {dependency_type} not registered.")
provider = self.dependencies[dependency_type]
instance = provider()
return instance
def singleton(self, dependency_type: Type[Any], provider: Callable[..., Any]):
self.register(dependency_type, provider)
self.instances[dependency_type] = provider()
# Example Dependencies
class PaymentGateway:
def process_payment(self, amount: float) -> bool:
print(f"Processing payment of ${amount}")
return True # Simulate successful payment
class NotificationService:
def send_notification(self, message: str):
print(f"Sending notification: {message}")
# Example Usage
container = Container()
container.singleton(PaymentGateway, PaymentGateway)
container.singleton(NotificationService, NotificationService)
app = FastAPI()
# FastAPI Dependency
def get_payment_gateway(payment_gateway: PaymentGateway = Depends(lambda: container.resolve(PaymentGateway))):
return payment_gateway
def get_notification_service(notification_service: NotificationService = Depends(lambda: container.resolve(NotificationService))):
return notification_service
@app.post("/purchase/")
async def purchase_item(payment_gateway: PaymentGateway = Depends(get_payment_gateway), notification_service: NotificationService = Depends(get_notification_service)):
if payment_gateway.process_payment(100.0):
notification_service.send_notification("Purchase successful!")
return {"message": "Purchase successful"}
else:
return {"message": "Purchase failed"}
شرح:
- تدير فئة
Containerقاموسًا بالتبعيات وموفريها. - تسجل طريقة
registerتبعية مع موفرها. - تعمل طريقة
resolveعلى حل تبعية عن طريق استدعاء موفرها. - تسجل طريقة
singletonتبعية وتنشئ مثيلًا واحدًا منها. - يتم إنشاء تبعيات FastAPI باستخدام دالة lambda لحل التبعيات من الحاوية.
3. استخدام `Depends` في FastAPI مع دالة المصنع
بدلاً من حاوية DI كاملة، يمكنك استخدام Depends في FastAPI مع وظائف المصنع لتحقيق بعض مستويات إدارة التبعية. هذا النهج أبسط من تنفيذ حاوية مخصصة ولكنه لا يزال يوفر بعض المزايا على تهيئة التبعيات مباشرةً داخل وظائف نقطة النهاية.
from fastapi import FastAPI, Depends
from typing import Callable
# Define Dependencies
class EmailService:
def __init__(self, smtp_server: str):
self.smtp_server = smtp_server
def send_email(self, recipient: str, subject: str, body: str):
print(f"Sending email to {recipient} via {self.smtp_server}: {subject} - {body}")
# Factory function for EmailService
def create_email_service(smtp_server: str) -> EmailService:
return EmailService(smtp_server=smtp_server)
# FastAPI
app = FastAPI()
# FastAPI Dependency, leveraging factory function and Depends
def get_email_service(email_service: EmailService = Depends(lambda: create_email_service(smtp_server="smtp.example.com"))):
return email_service
@app.post("/send-email/")
async def send_email(recipient: str, subject: str, body: str, email_service: EmailService = Depends(get_email_service)):
email_service.send_email(recipient=recipient, subject=subject, body=body)
return {"message": "Email sent!"}
شرح:
- نحدد دالة مصنع (
create_email_service) التي تنشئ مثيلات لتبعيةEmailService. - تستخدم تبعية
get_email_serviceDependsو lambda لاستدعاء دالة المصنع وتوفير مثيلEmailService. - تحقن دالة نقطة النهاية
send_emailتبعيةEmailService.
اعتبارات متقدمة
1. النطاقات ودورات الحياة
غالبًا ما توفر حاويات DI ميزات لإدارة دورة حياة التبعيات. تشمل النطاقات الشائعة:
- Singleton: يتم إنشاء مثيل واحد للتبعية وإعادة استخدامه طوال فترة حياة التطبيق. هذا مناسب للتبعية التي لا تحتوي على حالة أو لها نطاق عالمي.
- عابر: يتم إنشاء مثيل جديد للتبعية في كل مرة يتم فيها طلبها. هذا مناسب للتبعية التي تحتوي على حالة أو تحتاج إلى أن تكون معزولة عن بعضها البعض.
- طلب: يتم إنشاء مثيل واحد للتبعية لكل طلب وارد. هذا مناسب للتبعية التي تحتاج إلى الاحتفاظ بالحالة ضمن سياق طلب واحد.
توفر مكتبة dependency_injector دعمًا مضمنًا للنطاقات. بالنسبة للحاويات المخصصة، ستحتاج إلى تنفيذ منطق إدارة النطاق بنفسك.
2. التكوين
غالبًا ما تتطلب التبعيات إعدادات التكوين، مثل سلاسل اتصال قاعدة البيانات ومفاتيح واجهة برمجة التطبيقات وعلامات الميزات. يمكن أن تساعد حاويات DI في إدارة هذه الإعدادات من خلال توفير طريقة مركزية للوصول إلى قيم التكوين وحقنها.
في مثال dependency_injector، يسمح موفر config بالتكوين من متغيرات البيئة. بالنسبة للحاويات المخصصة، يمكنك تحميل التكوين من الملفات أو متغيرات البيئة وتخزينها في الحاوية.
3. الاختبار
تتمثل إحدى الفوائد الأساسية لـ DI في تحسين إمكانية الاختبار. باستخدام حاوية DI، يمكنك بسهولة استبدال التبعيات الحقيقية بكائنات وهمية أو مضاعفات اختبار أثناء الاختبار.
مثال (الاختبار باستخدام `dependency_injector`):
import pytest
from unittest.mock import MagicMock
from dependency_injector import containers, providers
from fastapi import FastAPI, Depends
from fastapi.testclient import TestClient
# Define dependencies (same as before)
class Database:
def __init__(self, connection_string: str):
self.connection_string = connection_string
def get_items(self):
return [{"id": 1, "name": "Item 1"}, {"id": 2, "name": "Item 2"}]
class UserRepository:
def __init__(self, database: Database):
self.database = database
def get_all_users(self):
return [{"id": "user1", "name": "Alice"},{"id": "user2", "name": "Bob"}]
class Settings:
def __init__(self, database_url):
self.database_url = database_url
# Define container (same as before)
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
settings = providers.Singleton(Settings, database_url = config.database_url)
database = providers.Singleton(Database, connection_string=config.database_url)
user_repository = providers.Factory(UserRepository, database=database)
# Create FastAPI app (same as before)
app = FastAPI()
# Configure container (from an environment variable)
container = Container()
container.config.database_url.from_env("DATABASE_URL", default="sqlite:///:memory:")
container.wire([__name__]) # enables injection of dependencies into FastAPI endpoints
# Dependency for FastAPI
def get_user_repository(user_repository: UserRepository = Depends(container.user_repository.provided)) -> UserRepository:
return user_repository
# Endpoint using injected dependency (same as before)
@app.get("/users/")
async def read_users(user_repository: UserRepository = Depends(get_user_repository)):
return user_repository.get_all_users()
@app.on_event("startup")
async def startup_event():
# Container initialization
container.init_resources()
# Test
@pytest.fixture
def test_client():
# Override the database dependency with a mock
database_mock = MagicMock(spec=Database)
database_mock.get_items.return_value = [{"id": 3, "name": "Test Item"}]
user_repository_mock = MagicMock(spec = UserRepository)
user_repository_mock.get_all_users.return_value = [{"id": "test_user", "name": "Test User"}]
# Override container with mock dependencies
container.user_repository.override(providers.Factory(lambda: user_repository_mock))
with TestClient(app) as client:
yield client
container.user_repository.reset()
def test_read_users(test_client: TestClient):
response = test_client.get("/users/")
assert response.status_code == 200
assert response.json() == [{"id": "test_user", "name": "Test User"}]
شرح:
- نحن نقوم بإنشاء كائن وهمي لتبعية
DatabaseباستخدامMagicMock. - نتجاوز موفر
databaseفي الحاوية بالكائن الوهمي باستخدامcontainer.database.override(). - تستخدم الآن دالة الاختبار
test_read_itemsتبعية قاعدة البيانات الوهمية. - بعد تنفيذ الاختبار، فإنه يعيد تعيين التبعية التي تم تجاوزها في الحاوية.
4. التبعيات غير المتزامنة
تم بناء FastAPI فوق البرمجة غير المتزامنة (async/await). عند العمل مع التبعيات غير المتزامنة (مثل اتصالات قاعدة البيانات غير المتزامنة)، تأكد من أن حاوية DI وموفري التبعية يدعمون العمليات غير المتزامنة.
مثال (التبعية غير المتزامنة مع `dependency_injector`):
import asyncio
from dependency_injector import containers, providers
from fastapi import FastAPI, Depends
# Define asynchronous dependency
class AsyncDatabase:
def __init__(self, connection_string: str):
self.connection_string = connection_string
async def connect(self):
print(f"Connecting to database: {self.connection_string}")
await asyncio.sleep(0.1) # Simulate connection time
async def fetch_data(self):
await asyncio.sleep(0.1) # Simulate database query
return [{"id": 1, "name": "Async Item 1"}]
# Define container
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
database = providers.Singleton(AsyncDatabase, connection_string=config.database_url)
# Create FastAPI app
app = FastAPI()
# Configure container
container = Container()
container.config.database_url.from_env("DATABASE_URL", default="sqlite:///:memory:")
container.wire([__name__])
# Dependency for FastAPI
async def get_async_database(database: AsyncDatabase = Depends(container.database.provided)) -> AsyncDatabase:
await database.connect()
return database
# Endpoint using injected dependency
@app.get("/async-items/")
async def read_async_items(database: AsyncDatabase = Depends(get_async_database)):
data = await database.fetch_data()
return data
@app.on_event("startup")
async def startup_event():
# Container initialization
container.init_resources()
شرح:
- تحدد فئة
AsyncDatabaseطرقًا غير متزامنة باستخدامasyncوawait. - يتم أيضًا تعريف التبعية
get_async_databaseكدالة غير متزامنة. - يتم تمييز دالة نقطة النهاية
read_async_itemsعلى أنهاasyncوتنتظر نتيجةdatabase.fetch_data().
اختيار النهج الصحيح
يعتمد أفضل نهج لبناء حاوية DI متقدمة على مدى تعقيد تطبيقك ومتطلباتك المحددة:
- للمشاريع الصغيرة إلى المتوسطة الحجم: قد يكون DI المدمج في FastAPI أو نهج دالة المصنع مع
Dependsكافياً. - للمشاريع الأكبر حجمًا والأكثر تعقيدًا: توفر مكتبة DI مخصصة مثل
dependency_injectorمجموعة شاملة من الميزات لإدارة التبعيات. - للمشاريع التي تتطلب تحكمًا دقيقًا في عملية DI: قد يكون تنفيذ حاوية DI مخصصة هو الخيار الأفضل.
خاتمة
حقن التبعية هو أسلوب قوي لبناء تطبيقات قابلة للتطوير والصيانة والاختبار. في حين أن نظام DI المدمج في FastAPI ممتاز لحالات الاستخدام البسيطة، يمكن لبنية حاوية DI المتقدمة أن توفر فوائد كبيرة للمشاريع الأكثر تعقيدًا. من خلال اختيار النهج الصحيح والاستفادة من ميزات مكتبات DI أو تنفيذ حاوية مخصصة، يمكنك إنشاء نظام إدارة تبعية قوي ومرن يحسن الجودة الشاملة وقابلية الصيانة لتطبيقات FastAPI الخاصة بك.
اعتبارات عالمية
عند تصميم حاويات DI للتطبيقات العالمية، من المهم مراعاة ما يلي:
- الترجمة: يجب أن تتم إدارة التبعيات المتعلقة بالترجمة (مثل إعدادات اللغة وتنسيقات التاريخ) بواسطة حاوية DI لضمان الاتساق عبر مناطق مختلفة.
- المناطق الزمنية: يجب حقن التبعيات التي تتعامل مع تحويلات المنطقة الزمنية لتجنب ترميز معلومات المنطقة الزمنية بشكل ثابت.
- العملة: يجب أن تتم إدارة تبعيات تحويل العملات وتنسيقها بواسطة الحاوية لدعم العملات المختلفة.
- الإعدادات الإقليمية: يجب أيضًا إدارة الإعدادات الإقليمية الأخرى، مثل تنسيقات الأرقام وتنسيقات العناوين، بواسطة حاوية DI.
- تعدد المستأجرين: بالنسبة للتطبيقات متعددة المستأجرين، يجب أن تكون حاوية DI قادرة على توفير تبعيات مختلفة لمستأجرين مختلفين. يمكن تحقيق ذلك باستخدام النطاقات أو منطق حل التبعية المخصص.
- الامتثال والأمان: تأكد من أن إستراتيجية إدارة التبعية الخاصة بك تتوافق مع لوائح خصوصية البيانات ذات الصلة (مثل GDPR و CCPA) وأفضل ممارسات الأمان في مناطق مختلفة. تعامل مع بيانات الاعتماد الحساسة والتكوينات بشكل آمن داخل الحاوية.
من خلال النظر في هذه العوامل العالمية، يمكنك إنشاء حاويات DI مناسبة جيدًا لبناء التطبيقات التي تعمل في بيئة عالمية.